﻿using SimpleORMNET.Attributes;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace SimpleORMNET
{
    internal static class Extensions
    {
        #region Public extension methods

        public static string GetInsertCommand<T>(this T @object, string tableName)
        {
            var sb = new StringBuilder();
            sb.AppendFormat("INSERT INTO {0} (", tableName);

            var columnsAndValues = @object.GetColumnsAndValuesCommand(true);
            int i = 1;
            foreach (var item in columnsAndValues)
            {
                sb.Append(item.Key);
                if (i++ < columnsAndValues.Count)
                    sb.Append(',');
            }
            sb.Append(") VALUES (");
            i = 1;
            foreach (var item in columnsAndValues)
            {
                sb.Append(item.Value);
                if (i++ < columnsAndValues.Count)
                    sb.Append(',');
            }
            sb.Append(")");

            return sb.ToString();
        }

        public static string GetUpdateCommand<T>(this T @object, string tableName)
        {
            var sb = new StringBuilder();
            sb.AppendFormat("UPDATE {0} SET ", tableName);

            var columnsAndValues = @object.GetColumnsAndValuesCommand();
            int i = 1;
            foreach (var item in columnsAndValues)
            {
                sb.AppendFormat("{0} = {1}", item.Key, item.Value);
                if (i++ < columnsAndValues.Count)
                    sb.Append(',');
            }

            var pimaryKeyColumn = @object.GetPrimaryKeyColumnName();
            var primaryKeyValue = @object.GetPrimaryKeyPropertyValue();
            sb.AppendFormat(" WHERE {0} = {1}", pimaryKeyColumn, primaryKeyValue);

            return sb.ToString();
        }

        public static string GetDeleteCommand<T>(this T @object, string tableName)
        {
            var sb = new StringBuilder();

            var pimaryKeyColumn = @object.GetPrimaryKeyColumnName();
            var primaryKeyValue = @object.GetPrimaryKeyPropertyValue();

            sb.AppendFormat("DELETE FROM {0} WHERE {1} = {2}", tableName, pimaryKeyColumn, primaryKeyValue);

            return sb.ToString();
        }

        public static PropertyInfo GetProperty(this PropertyInfo[] properties, string columnName)
        {
            foreach (var property in properties)
            {
                var attributes = property.GetCustomAttributes<Column>();
                var attr = attributes.FirstOrDefault(a => a.Name == columnName);
                if (attr != null)
                {
                    return property;
                }
            }

            return properties.FirstOrDefault(p => p.Name == columnName);
        }

        public static IEnumerable<TResult> GetMappedRows<TResult>(this SqlDataReader reader) where TResult : class, new()
        {
            while (reader.Read())
            {
                yield return reader.MapRow<TResult>();
            }
        }

        public static TResult MapRow<TResult>(this SqlDataReader reader) where TResult : class, new()
        {
            var constructor = typeof(TResult).GetConstructors().First();
            var model = constructor.Invoke(null) as TResult;
            var properties = model.GetType().GetProperties();

            for (int i = 0; i < reader.FieldCount; i++)
            {
                var name = reader.GetName(i);
                var value = reader[i];

                if (value is DBNull)
                {
                    value = null;
                }

                var property = properties.GetProperty(name);
                if (property != null)
                {
                    try
                    {
                        object finalValue = DeserializeIfNeeded(property, value);
                        property.SetValue(model, finalValue);
                    }
                    catch (ArgumentException argEx)
                    {
                        string message = String.Format("Type mismatch between column {0} and property {1}. Expected type: {2}", name, property.Name, value.GetType());
                        throw new TypeMismatchException(message, argEx);
                    }
                }
            }

            return model;
        }

        #endregion Public extension methods

        #region Private methods

        private static string GetPrimaryKeyColumnName<T>(this T @object)
        {
            var properties = typeof(T).GetProperties();

            foreach (var property in properties)
            {
                var attributes = property.GetCustomAttributes<PrimaryKey>();
                if (attributes.Any())
                {
                    var columnAttributes = property.GetCustomAttributes<Column>();
                    if (columnAttributes.Any())
                        return columnAttributes.First().Name;
                    return property.Name;
                }
            }

            throw new NoPrimaryKeyException("No primary key attribute in data model class");
        }

        private static string GetPrimaryKeyPropertyValue<T>(this T @object)
        {
            var properties = typeof(T).GetProperties();

            foreach (var property in properties)
            {
                var attributes = property.GetCustomAttributes<PrimaryKey>();
                if (attributes.Any())
                {
                    var valueObject = property.GetValue(@object);
                    if (valueObject == null)
                    {
                        throw new NoPrimaryKeyException("Primary key value is null");
                    }
                    if (property.PropertyType.IsValueType)
                        return valueObject.ToString();
                    else
                        return String.Format("\'{0}\'", valueObject);
                }
            }

            throw new NoPrimaryKeyException("No primary key attribute in data model class");
        }

        private static Dictionary<string, string> GetColumnsAndValuesCommand<T>(this T @object, bool isInsertCommand = false)
        {
            var result = new Dictionary<string, string>();

            var properties = typeof(T).GetProperties();

            foreach (var property in properties)
            {
                string key, value;

                // we don't update a primary column or a column that is marked as ignored
                if (isInsertCommand)
                {
                    var ignoreOnInsertAttributes = property.GetCustomAttributes<IgnoreOnInsert>();
                    if (ignoreOnInsertAttributes.Any())
                        continue;
                }
                else
                {
                    var primaryKeyAttributes = property.GetCustomAttributes<PrimaryKey>();
                    var ignoreOnUpdateAttributes = property.GetCustomAttributes<IgnoreOnUpdate>();
                    if (primaryKeyAttributes.Any() || ignoreOnUpdateAttributes.Any())
                        continue;
                }

                // first we map values based on Column attributes, then by property names
                var attributes = property.GetCustomAttributes<Column>();
                if (attributes.Any())
                    key = attributes.First().Name;
                else
                    key = property.Name;

                var valueObject = property.GetValue(@object);
                var serializeXmlAttributes = property.GetCustomAttributes<SerializeXml>();

                if (valueObject == null)
                {
                    value = "NULL";
                }
                else if (serializeXmlAttributes.Any()) // serialize if needed
                {
                    var type = serializeXmlAttributes.First().Type;
                    var serializedValue = valueObject.SerializeToString(type);
                    value = String.Format("N\'{0}\'", serializedValue);
                }
                else
                {
                    if (property.PropertyType.IsValueType)
                        value = valueObject.ToString();
                    else
                        value = String.Format("\'{0}\'", valueObject);
                }

                result.Add(key, value);
            }

            return result;
        }

        private static object DeserializeIfNeeded(PropertyInfo property, object value)
        {
            if (!(value is String))
                return value;

            var deserializeXmlAttributes = property.GetCustomAttributes<SerializeXml>();
            var deserializeXmlAttr = deserializeXmlAttributes.FirstOrDefault();
            if (deserializeXmlAttr != null)
            {
                var type = deserializeXmlAttr.Type;
                var xmlSerializer = new XmlSerializer(type);
                object result;
                using (var reader = new StringReader(value as String))
                {
                    result = xmlSerializer.Deserialize(reader);
                }
                return result;
            }

            return value;
        }

        private static string SerializeToString(this object @object, Type type)
        {
            var emptyNamepsaces = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });
            var xmlSerializer = new XmlSerializer(type);
            var settings = new XmlWriterSettings()
            {
                Indent = false,
                OmitXmlDeclaration = true
            };

            using (var stream = new StringWriter())
            using (var writer = XmlWriter.Create(stream, settings))
            {
                xmlSerializer.Serialize(writer, @object, emptyNamepsaces);
                return stream.ToString();
            }
        }

        #endregion Private methods
    }
}